1"use client";
2
3
12import { useEffect, useState } from "react";
13import { Moon, Monitor, Sun } from "lucide-react";
14import { Switch } from "@factory/ui";
15import { useUiStore, Theme } from "@/stores/ui-store";
16import { cn } from "@/lib/utils";
17import { SettingsShell, Section, Field } from "@/components/settings/settings-shell";
18
19const THEME_OPTIONS: { value: Theme; label: string; icon: typeof Sun }[] = [
20 { value: "light", label: "Light", icon: Sun },
21 { value: "dark", label: "Dark", icon: Moon },
22 { value: "system", label: "System", icon: Monitor },
23];
24
25const DENSITIES = ["comfortable", "compact"] as const;
26const ACCENTS = [
27 { value: "#6366f1", label: "Indigo" },
28 { value: "#0ea5e9", label: "Sky" },
29 { value: "#10b981", label: "Emerald" },
30 { value: "#f59e0b", label: "Amber" },
31 { value: "#ef4444", label: "Red" },
32 { value: "#8b5cf6", label: "Violet" },
33] as const;
34
35export default function SettingsAppearancePage() {
36 const theme = useUiStore((s) => s.theme);
37 const setTheme = useUiStore((s) => s.setTheme);
38 const presenceToastsEnabled = useUiStore((s) => s.presenceToastsEnabled);
39 const setPresenceToastsEnabled = useUiStore((s) => s.setPresenceToastsEnabled);
40
41 const [density, setDensity] = useState<(typeof DENSITIES)[number]>("comfortable");
42 const [accent, setAccent] = useState<string>("#6366f1");
43
44
45 useEffect(() => {
46 const root = document.documentElement;
47 root.style.setProperty("--factory-density", density);
48 root.style.setProperty("--factory-accent", accent);
49 }, [density, accent]);
50
51 return (
52 <SettingsShell
53 title="Appearance"
54 description="Personalize how the workspace looks for you."
55 active="appearance"
56 >
57 <div className="space-y-5">
58 <Section title="Theme">
59 <div className="grid grid-cols-3 gap-2">
60 {THEME_OPTIONS.map((o) => {
61 const active = theme === o.value;
62 return (
63 <button
64 key={o.value}
65 type="button"
66 onClick={() => setTheme(o.value)}
67 aria-pressed={active}
68 className={cn(
69 "flex flex-col items-center gap-2 rounded-lg border p-3 text-sm transition-colors",
70 active
71 ? "border-primary bg-primary/5 font-medium text-foreground"
72 : "border-border text-muted-foreground hover:border-primary/40",
73 )}
74 >
75 <o.icon className="size-5" aria-hidden />
76 {o.label}
77 </button>
78 );
79 })}
80 </div>
81 {theme === "system" && (
82 <p className="mt-2 text-xs text-muted-foreground">
83 Follows your operating system's light/dark preference.
84 </p>
85 )}
86 </Section>
87
88 <Section
89 title="Presence notifications"
90 description="Show a toast when someone joins or leaves a page you're viewing. Screen readers always hear join/edit/leave announcements (they can't be muted here)."
91 >
92 <Field label="Join/leave notifications" hint="ON by default.">
93 <Switch
94 checked={presenceToastsEnabled}
95 onCheckedChange={setPresenceToastsEnabled}
96 aria-label="Join and leave presence notifications"
97 />
98 </Field>
99 </Section>
100
101 <Section
102 title="Density"
103 description="How much space blocks get. (UI-only; not persisted yet.)"
104 >
105 <div className="flex gap-2">
106 {DENSITIES.map((d) => (
107 <button
108 key={d}
109 type="button"
110 onClick={() => setDensity(d)}
111 aria-pressed={density === d}
112 className={cn(
113 "rounded-md border px-3 py-1.5 text-sm capitalize",
114 density === d
115 ? "border-primary bg-primary/5 font-medium"
116 : "border-border text-muted-foreground hover:border-primary/40",
117 )}
118 >
119 {d}
120 </button>
121 ))}
122 </div>
123 </Section>
124
125 <Section title="Accent" description="Primary accent color. (UI-only; not persisted yet.)">
126 <div className="flex flex-wrap gap-2">
127 {ACCENTS.map((a) => (
128 <button
129 key={a.value}
130 type="button"
131 onClick={() => setAccent(a.value)}
132 aria-pressed={accent === a.value}
133 title={a.label}
134 className={cn(
135 "flex items-center gap-1.5 rounded-md border px-2 py-1 text-xs",
136 accent === a.value
137 ? "border-primary font-medium"
138 : "border-border text-muted-foreground hover:border-primary/40",
139 )}
140 >
141 <span
142 className="size-4 rounded-full border border-black/10"
143 style={{ backgroundColor: a.value }}
144 aria-hidden
145 />
146 {a.label}
147 </button>
148 ))}
149 </div>
150 </Section>
151 </div>
152 </SettingsShell>
153 );
154}
155